﻿using System;
using System.Windows.Controls;
using System.Windows;
using AvalonDock;
using System.Collections.Generic;
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using System.ComponentModel;

namespace AvalonDockMVVM
{
	/// <summary>
	/// Interaction logic for AvalonDockHost.xaml
	/// </summary>
	public partial class AvalonDockHost : UserControl
	{
		#region Dependency Properties 

		public static readonly DependencyProperty PanesProperty =
			DependencyProperty.Register("Panes", typeof(IList), typeof(AvalonDockHost),
				new FrameworkPropertyMetadata(DocumentsOrPanes_PropertyChanged));

		public static readonly DependencyProperty DocumentsProperty =
			DependencyProperty.Register("Documents", typeof(IList), typeof(AvalonDockHost),
				new FrameworkPropertyMetadata(DocumentsOrPanes_PropertyChanged));

		public static readonly DependencyProperty ActiveDocumentProperty =
			DependencyProperty.Register("ActiveDocument", typeof(object), typeof(AvalonDockHost),
				new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ActiveDocumentOrPane_PropertyChanged));

		public static readonly DependencyProperty ActivePaneProperty =
			DependencyProperty.Register("ActivePane", typeof(object), typeof(AvalonDockHost),
				new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ActiveDocumentOrPane_PropertyChanged));

		//
		// Attached properties.
		//

		public static readonly DependencyProperty IsPaneVisibleProperty =
			DependencyProperty.RegisterAttached("IsPaneVisible", typeof(bool), typeof(AvalonDockHost),
				new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, IsPaneVisible_PropertyChanged));

		#endregion Dependency Properties

		#region Private Data Members

		/// <summary>
		/// A dictionary that maps from view-model object to Avalondock ManagedContent object.
		/// </summary>
		private Dictionary<object, ManagedContent> contentMap = new Dictionary<object, ManagedContent>();

		/// <summary>
		/// Set to 'true' to disable the request for user confirmation while closing a document.
		/// </summary>
		private bool disableClosingEvent = false;

		#endregion Private Data Members

		public AvalonDockHost()
		{
			InitializeComponent();

			//
			// Hook the AvalonDock event that is raised when the focused content is changed.
			//
			dockingManager.ActiveContentChanged += new EventHandler(dockingManager_ActiveContentChanged);

			UpdateActiveContent();
		}

		/// <summary>
		/// The collection of view-model objects for panes.
		/// Adding an object to this collection results in 
		/// a pane being added to AvalonDock.  There must be
		/// a DataTemplate for the view-model defined within
		/// the resources of the visual-tree and the 
		/// DataTemplate should contain an AvalonDock
		/// DockableControl.
		/// NOTE: This property is initially null, you should 
		/// either assign a collection to it or, as it is
		/// intended to be used, data-bind it to a collection
		/// in the view-model.
		/// </summary>
		public IList Panes
		{
			get
			{
				return (IList)GetValue(PanesProperty);
			}
			set
			{
				SetValue(PanesProperty, value);
			}
		}

		/// <summary>
		/// The collection of view-model objects for documents.
		/// Adding an object to this collection results in 
		/// a document being added to AvalonDock.  There must be
		/// a DataTemplate for the view-model defined within
		/// the resources of the visual-tree and the 
		/// DataTemplate should contain an AvalonDock
		/// DocumentControl.
		/// NOTE: This property is initially null, you should 
		/// either assign a collection to it or, as it is
		/// intended to be used, data-bind it to a collection
		/// in the view-model.
		/// </summary>
		public IList Documents
		{
			get
			{
				return (IList)GetValue(DocumentsProperty);
			}
			set
			{
				SetValue(DocumentsProperty, value);
			}
		}

		/// <summary>
		/// The view-model object for the currently active document.
		/// This can programatically set to change the active document,
		/// or it can be data-bound to the view-model so that the active
		/// document can be changed via the view-model.
		/// It is also used to retieve the currently active document's view-model
		/// and is automatically updated when the user interacts directly with
		/// AvalonDock selecting a tabbed-document.
		/// </summary>
		public object ActiveDocument
		{
			get
			{
				return (object)GetValue(ActiveDocumentProperty);
			}
			set
			{
				SetValue(ActiveDocumentProperty, value);
			}
		}

		/// <summary>
		/// The view-model object for the currently active pane.
		/// This can programatically set to change the active pane,
		/// or it can be data-bound to the view-model so that the active
		/// pane can be changed via the view-model.
		/// It is also used to retieve the currently active pane's view-model
		/// and is automatically updated when the user interacts directly with
		/// AvalonDock selecting a pane.
		/// </summary>
		public object ActivePane
		{
			get
			{
				return (object)GetValue(ActivePaneProperty);
			}
			set
			{
				SetValue(ActivePaneProperty, value);
			}
		}

		/// <summary>
		/// Allow access to the Avalondock DockingManager.
		/// This class isn't a complete wrapper, 
		/// it should be considered a helper that provides MVVM support for Avalondock.
		/// </summary>
		public DockingManager DockingManager
		{
			get
			{
				return dockingManager;
			}
		}

		/// <summary>
		/// Event raised when Avalondock has loaded.
		/// </summary>
		public event EventHandler<EventArgs> AvalonDockLoaded;

		/// <summary>
		/// Event raised when a document is being closed by clicking on the AvalonDock X button .
		/// </summary>
		public event EventHandler<DocumentClosingEventArgs> DocumentClosing;

		/// <summary>
		/// Sets the IsPaneVisible for an element.
		/// </summary>
		public static void SetIsPaneVisible(UIElement element, bool value)
		{
			element.SetValue(IsPaneVisibleProperty, value);
		}

		/// <summary>
		/// Gets the IsPaneVisible property for an element.
		/// </summary>
		public static bool GetIsPaneVisible(UIElement element)
		{
			return (bool)element.GetValue(IsPaneVisibleProperty);
		}

		#region Private Methods

		/// <summary>
		/// Event raised when the Documents or Panes properties have changed.
		/// </summary>
		private static void DocumentsOrPanes_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var c = (AvalonDockHost)d;

			//
			// Deal with the previous value of the property.
			//
			if (e.OldValue != null)
			{
				//
				// Remove the old panels from AvalonDock.
				//
				var oldPanels = (IList)e.OldValue;
				c.RemovePanels(oldPanels);

				var observableCollection = oldPanels as INotifyCollectionChanged;
				if (observableCollection != null)
				{
					//
					// Unhook the CollectionChanged event, we no longer need to receive notifications
					// of modifications to the collection.
					//
					observableCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler(c.documentsOrPanes_CollectionChanged);
				}
			}

			//
			// Deal with the new value of the property.
			//
			if (e.NewValue != null)
			{
				//
				// Add the new panels to AvalonDock.
				//
				var newPanels = (IList)e.NewValue;
				c.AddPanels(newPanels);

				var observableCollection = newPanels as INotifyCollectionChanged;
				if (observableCollection != null)
				{
					//
					// Hook the CollectionChanged event to receive notifications
					// of future modifications to the collection.
					//
					observableCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(c.documentsOrPanes_CollectionChanged);
				}
			}
		}

		/// <summary>
		/// Event raised when the 'Documents' or 'Panes' collection have had items added/removed.
		/// </summary>
		private void documentsOrPanes_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			if (e.Action == NotifyCollectionChangedAction.Reset)
			{
				//
				// The collection has been cleared, need to remove all
				// documents or panes from AvalonDock depending on which collection was
				// actually cleared.
				//
				ResetDocumentsOrPanes(sender);
			}
			else
			{
				if (e.OldItems != null)
				{
					//
					// Remove the old panels from AvalonDock.
					//
					RemovePanels(e.OldItems);
				}

				if (e.NewItems != null)
				{
					//
					// Add the new panels to AvalonDock.
					//
					AddPanels(e.NewItems);
				}
			}
		}

		/// <summary>
		/// This method resets documents or panes in response
		/// to the Reset action of the CollectionChanged events.
		/// </summary>
		private void ResetDocumentsOrPanes(object sender)
		{
			if (sender == this.Documents)
			{
				//
				// We are clearing the collection of documents.
				//
				foreach (var item in this.contentMap.ToArray())
				{
					if (item.Value is DocumentContent)
					{
						//
						// Only remove DocumentContent components.
						//
						RemovePanel(item.Key);
					}
				}
			}
			else if (sender == this.Panes)
			{
				//
				// We are clearing the collection of panes.
				//
				foreach (var item in this.contentMap.ToArray())
				{
					if (item.Value is DockableContent)
					{
						//
						// Only remove DockableContent components.
						//
						RemovePanel(item.Key);
					}
				}
			}
			else
			{
				throw new ApplicationException("Unexpected CollectionChanged event sender: " + sender.GetType().Name);
			}
		}

		/// <summary>
		/// Add panels to Avalondock.
		/// </summary>
		private void AddPanels(IList panels)
		{
			foreach (var panel in panels)
			{
				AddPanel(panel);
			}
		}

		/// <summary>
		/// Add a panel to Avalondock.
		/// </summary>
		private void AddPanel(object panel)
		{
			//
			// Instantiate a UI element based on the panel's view-model type.
			// The visual-tree is searched to find a DataTemplate keyed to the requested type.
			//
			var panelViewModelType = panel.GetType();
			var uiElement = DataTemplateUtils.InstanceTemplate(panelViewModelType, this, panel);
			if (uiElement == null)
			{
				throw new ApplicationException("Failed to find data-template for type: " + panel.GetType().Name);
			}

			//
			// Cast the instantiated UI element to an AvalonDock ManagedContent.
			// ManagedContent can refer to either an AvalonDock DocumentContent
			// or an AvalonDock DockableContent.
			//
			var managedContent = uiElement as ManagedContent;
			if (managedContent == null)
			{
				throw new ApplicationException("Found data-template for type: " + panel.GetType().Name + ", but the UI element generated is not a ManagedContent (base-class of DocumentContent/DockableContent), rather it is a " + uiElement.GetType().Name);
			}

			//
			// Associate the panel's view-model with the Avalondock ManagedContent so it can be retrieved later.
			//
			contentMap[panel] = managedContent;

			//
			// Hook the event to track when the document has been closed.
			//
			managedContent.Closed += new EventHandler(managedContent_Closed);

			var documentContent = managedContent as DocumentContent;
			if (documentContent != null)
			{
				//
				// For documents only, hook Closing so that the application can be informed
				// when a document is in the process of being closed by the use clicking the
				// AvalonDock close button.
				//
				documentContent.Closing += new EventHandler<CancelEventArgs>(documentContent_Closing);

				documentContent.Show(dockingManager);
				documentContent.Activate();
			}
			else
			{
				var dockableContent = managedContent as DockableContent;
				if (dockableContent != null)
				{
					//
					// For panes only, hook StateChanged so we know when a DockableContent is shown/hidden.
					//
					dockableContent.StateChanged += new RoutedEventHandler(dockableContent_StateChanged);
					dockableContent.ShowAsDocument(dockingManager);
					dockableContent.Activate();
				}
				else
				{
					throw new ApplicationException("Panel " + managedContent.GetType().Name + " is expected to be either DocumentContent or DockableContent."); 
				}
			}
		}

		/// <summary>
		/// Remove panels from Avalondock.
		/// </summary>
		private void RemovePanels(IList panels)
		{
			foreach (var panel in panels)
			{
				RemovePanel(panel);
			}
		}

		/// <summary>
		/// Remove a panel from Avalondock.
		/// </summary>
		private void RemovePanel(object panel)
		{
			//
			// Look up the document in the content map.
			//
			ManagedContent managedContent = null;
			if (contentMap.TryGetValue(panel, out managedContent))
			{
				disableClosingEvent = true;

				try
				{
					//
					// The content was still in the map, and therefore still open, so close it.
					//
					managedContent.Close();
				}
				finally
				{
					disableClosingEvent = false;
				}
			}
		}

		/// <summary>
		/// Event raised when an AvalonDock DocumentContent is being closed.
		/// </summary>
		private void documentContent_Closing(object sender, CancelEventArgs e)
		{
			var documentContent = (DocumentContent)sender;
			var document = documentContent.DataContext;

			if (!disableClosingEvent)
			{
				if (this.DocumentClosing != null)
				{
					//
					// Notify the application that the document is being closed.
					//
					var eventArgs = new DocumentClosingEventArgs(document);
					this.DocumentClosing(this, eventArgs);

					if (eventArgs.Cancel)
					{
						//
						// Closing of the document is to be cancelled.
						//
						e.Cancel = true;
						return;
					}
				}
			}

			documentContent.Closing -= new EventHandler<CancelEventArgs>(documentContent_Closing);
		}

		/// <summary>
		/// Event raised when an Avalondock ManagedContent has been closed.
		/// </summary>
		private void managedContent_Closed(object sender, EventArgs e)
		{
			var managedContent = (ManagedContent)sender;
			var content = managedContent.DataContext;

			//
			// Remove the content from the content map right now.
			// There is no need to keep it around any longer.
			//
			contentMap.Remove(content);

			managedContent.Closed -= new EventHandler(managedContent_Closed);

			var documentContent = managedContent as DocumentContent;
			if (documentContent != null)
			{
				this.Documents.Remove(content);

				if (this.ActiveDocument == content)
				{
					//
					// Active document has closed, clear it.
					//
					this.ActiveDocument = null;
				}
			}
			else
			{
				var dockableContent = managedContent as DockableContent;
				if (dockableContent != null)
				{
					//
					// For panes only, unhook StateChanged event.
					//
					dockableContent.StateChanged -= new RoutedEventHandler(dockableContent_StateChanged);

					this.Panes.Remove(content);

					if (this.ActivePane == content)
					{
						//
						// Active pane has closed, clear it.
						//
						this.ActivePane = null;
					}
				}
			}
		}

		/// <summary>
		/// Event raised when the 'dockable state' of a DockableContent has changed.
		/// </summary>
		private void dockableContent_StateChanged(object sender, RoutedEventArgs e)
		{
			var dockableContent = (DockableContent)sender;
			SetIsPaneVisible(dockableContent, dockableContent.State != DockableContentState.Hidden);
		}

		/// <summary>
		/// Update the active pane and document from the currently active AvalonDock component.
		/// </summary>
		private void UpdateActiveContent()
		{
			var activePane = dockingManager.ActiveContent as DockableContent;
			if (activePane != null)
			{
				//
				// Set the active document so that we can bind to it.
				//
				this.ActivePane = activePane.DataContext;
			}
			else
			{
				var activeDocument = dockingManager.ActiveContent as DocumentContent;
				if (activeDocument != null)
				{
					//
					// Set the active document so that we can bind to it.
					//
					this.ActiveDocument = activeDocument.DataContext;
				}
			}
		}

		/// <summary>
		/// Event raised when the IsPaneVisible property changes.
		/// </summary>
		private static void IsPaneVisible_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
		{
			var avalonDockContent = o as ManagedContent;
			if (avalonDockContent != null)
			{
				bool isVisible = (bool)e.NewValue;
				if (isVisible)
				{
					avalonDockContent.Show();
				}
				else
				{
					avalonDockContent.Hide();
				}
			}
		}

		/// <summary>
		/// Event raised when the active content has changed.
		/// </summary>
		private void dockingManager_ActiveContentChanged(object sender, EventArgs e)
		{
			UpdateActiveContent();
		}

		/// <summary>
		/// Event raised when the ActiveDocument or ActivePane property has changed.
		/// </summary>
		private static void ActiveDocumentOrPane_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var c = (AvalonDockHost)d;
			ManagedContent managedContent = null;
			if (e.NewValue != null &&
				c.contentMap.TryGetValue(e.NewValue, out managedContent))
			{
				managedContent.Activate();
			}
		}

		/// <summary>
		/// Event raised when Avalondock has loaded.
		/// </summary>
		private void AvalonDock_Loaded(object sender, RoutedEventArgs e)
		{
			if (AvalonDockLoaded != null)
			{
				AvalonDockLoaded(this, EventArgs.Empty);
			}
		}

		#endregion Private Methods
	}
}
